跳到主要内容

Java IO学习-NIO 文件编程

FileChannel

FileChannel 只能在阻塞模式下工作,所以无法搭配 Selector

获取 FileChannel

获取不能直接打开 FileChannel,必须通过 FileInputStream、FileOutputStream 或者 RandomAccessFile 来获取 FileChannel,它们都有 getChannel 方法

  • 通过 FileInputStream 获取的 channel 只能读
  • 通过 FileOutputStream 获取的 channel 只能写
  • 通过 RandomAccessFile 是否能读写根据构造 RandomAccessFile 时的读写模式决定

读取数据

通过 FileInputStream 获取 channel,通过 read 方法将数据写入到 ByteBuffer 中,其中 read 方法的返回值表示读到了多少字节,若读到了文件末尾则返回 -1

int readBytes = channel.read(buffer);

可根据返回值判断是否读取完毕

while(channel.read(buffer) > 0) {
// 进行对应操作
...
}

写入数据

因为 channel 也是有大小的,所以 write 方法并不能保证一次将 buffer 中的内容全部写入 channel。必须需要按照以下规则进行写入

// 通过 hasRemaining() 方法查看缓冲区中是否还有数据未写入到通道中
while(buffer.hasRemaining()) {
channel.write(buffer);
}

关闭通道

通道需要 close,一般情况通过 try-with-resource 进行关闭,最好使用以下方法获取 stream 以及 channel,避免某些原因使得资源未被关闭

public class TestChannel {
public static void main(String[] args) throws IOException {
try (FileInputStream fis = new FileInputStream("stu.txt");
FileOutputStream fos = new FileOutputStream("student.txt");
FileChannel inputChannel = fis.getChannel();
FileChannel outputChannel = fos.getChannel()) {

// 执行对应操作
...

}
}
}

操作 position

channel 也拥有一个保存读取数据位置的属性,即 position

long pos = channel.position();

可以通过 position(int pos) 设置 channel 中 position 的值

long newPos = ...;
channel.position(newPos);

设置当前位置时,如果设置为文件的末尾

  • 这时读取会返回 -1
  • 这时写入,会追加内容,但要注意如果 position 超过了文件末尾,再写入时在新内容和原末尾之间会有空洞(00)

强制写入

操作系统出于性能的考虑,会将数据缓存,不是立刻写入磁盘,而是等到缓存满了以后将所有数据一次性的写入磁盘。

可以调用 force(true) 方法将文件内容和元数据(文件的权限等信息)立刻写入磁盘

Channel 传输数据

transferTo 方法(零拷贝)

使用 transferTo 方法可以快速、高效地将一个 channel 中的数据传输到另一个 channel 中,但一次只能传输 2G 的内容

transferTo 底层使用了零拷贝技术

public class TestChannel {
public static void main(String[] args){
try (FileInputStream fis = new FileInputStream("stu.txt");
FileOutputStream fos = new FileOutputStream("student.txt");
FileChannel inputChannel = fis.getChannel();
FileChannel outputChannel = fos.getChannel()) {
// 参数:inputChannel 的起始位置,传输数据的大小,目的 channel
// 返回值为传输的数据的字节数
// transferTo 一次只能传输 2G 的数据
inputChannel.transferTo(0, inputChannel.size(), outputChannel);
} catch (IOException e) {
e.printStackTrace();
}
}
}

文件大于 2G 的传输

当传输的文件大于 2G 时(就不适合使用零拷贝了),需要使用以下方法进行多次传输

public class TestChannel {
public static void main(String[] args){
try (FileInputStream fis = new FileInputStream("stu.txt");
FileOutputStream fos = new FileOutputStream("student.txt");
FileChannel inputChannel = fis.getChannel();
FileChannel outputChannel = fos.getChannel()) {
long size = inputChannel.size();
long capacity = inputChannel.size();
// 分多次传输
while (capacity > 0) {
// transferTo 返回值为传输了的字节数
capacity -= inputChannel.transferTo(size - capacity, capacity, outputChannel);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

Path 与 Paths 工具类

  • Path 用来表示文件路径
  • Paths 是工具类,用来获取 Path 实例
Path source = Paths.get("1.txt"); // 相对路径 不带盘符 使用 user.dir 环境变量来定位 1.txt
Path source = Paths.get("d:\\1.txt"); // 绝对路径 代表了 d:\1.txt 反斜杠需要转义
Path source = Paths.get("d:/1.txt"); // 绝对路径 同样代表了 d:\1.txt
Path projects = Paths.get("d:\\data", "projects"); // 代表了 d:\data\projects
  • . 代表了当前路径
  • .. 代表了上一级路径

例如目录结构如下

d:
|- data
|- projects
|- a
|- b

代码:

Path path = Paths.get("d:\\data\\projects\\a\\..\\b");
System.out.println(path);
System.out.println(path.normalize()); // 正常化路径 会去除 . 以及 ..

输出结果为

d:\data\projects\a\..\b
d:\data\projects\b

Files 工具类

查找是否存在

Path path = Paths.get("helloword/data.txt");
System.out.println(Files.exists(path));

创建一级目录

Path path = Paths.get("helloword/d1");
Files.createDirectory(path);

如果目录已存在,会抛异常 FileAlreadyExistsException 不能一次创建多级目录,否则会抛异常 NoSuchFileException

创建多级目录用

Path path = Paths.get("helloword/d1/d2");
Files.createDirectories(path);

拷贝文件

Path source = Paths.get("helloword/data.txt");
Path target = Paths.get("helloword/target.txt");

Files.copy(source, target);

如果文件已存在,会抛异常 FileAlreadyExistsException

如果希望用 source 覆盖掉 target,需要用 StandardCopyOption 来控制

Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);

移动文件

Path source = Paths.get("helloword/data.txt");
Path target = Paths.get("helloword/data.txt");

Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);

这个 StandardCopyOption.ATOMIC_MOVE 保证文件移动的原子性

删除文件

Path target = Paths.get("helloword/target.txt");
Files.delete(target);

如果文件不存在,会抛异常 NoSuchFileException

删除目录

Path target = Paths.get("helloword/d1");
Files.delete(target);

如果目录还有内容,会抛异常 DirectoryNotEmptyException

遍历文件

可以使用 Files 工具类中的 walkFileTree(Path, FileVisitor) 方法,其中需要传入两个参数

  • Path:文件起始路径
  • FileVisitor:文件访问器,使用模板方法模式

接口的实现类 SimpleFileVisitor 有四个方法

  • preVisitDirectory:访问目录前的操作
  • visitFile:访问文件的操作
  • visitFileFailed:访问文件失败时的操作
  • postVisitDirectory:访问目录后的操作
public class TestWalkFileTree {
public static void main(String[] args) throws IOException {
Path path = Paths.get("F:\\JDK 8");
// 文件目录数目
AtomicInteger dirCount = new AtomicInteger();
// 文件数目
AtomicInteger fileCount = new AtomicInteger();
Files.walkFileTree(path, new SimpleFileVisitor<Path>(){
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
System.out.println("===>"+dir);
// 增加文件目录数
dirCount.incrementAndGet();
return super.preVisitDirectory(dir, attrs);
}

@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println(file);
// 增加文件数
fileCount.incrementAndGet();
return super.visitFile(file, attrs);
}
});
// 打印数目
System.out.println("文件目录数:"+dirCount.get());
System.out.println("文件数:"+fileCount.get());
}
}

运行结果如下

...
===>F:\JDK 8\lib\security\policy\unlimited
F:\JDK 8\lib\security\policy\unlimited\local_policy.jar
F:\JDK 8\lib\security\policy\unlimited\US_export_policy.jar
F:\JDK 8\lib\security\trusted.libraries
F:\JDK 8\lib\sound.properties
F:\JDK 8\lib\tzdb.dat
F:\JDK 8\lib\tzmappings
F:\JDK 8\LICENSE
F:\JDK 8\README.txt
F:\JDK 8\release
F:\JDK 8\THIRDPARTYLICENSEREADME-JAVAFX.txt
F:\JDK 8\THIRDPARTYLICENSEREADME.txt
F:\JDK 8\Welcome.html
文件目录数:23
文件数:279